<?php
namespace zwx\daemon\console;
/**
 * daemon
 * Created by PhpStorm.
 * User: shaoxia
 * Date: 19-12-5
 * Time: 下午2:30
 */
use yii\console\Controller;
use yii;

class DaemonController extends Controller
{
    /**
     * @var string $pidFile file to save pid
     */
    public $pidFile;

    /**
     * @var array $daemonConfig
     */
    public $daemonConfig = [];

    public $infoLog = '';

    public $errLog = '';

    /**
     * @var array $running_children_pid
     */
    private $running_children_pid = [];

    /**
     * @var bool $running
     */
    private $running = true;

    /**
     * @var int $_master_pid pid
     */
    private static $_master_pid;

    public function beforeAction($action)
    {
        if (php_sapi_name() != "cli") {
            die("only run in command line mode\n");
        }
        $this->_checkPcntl();
        return parent::beforeAction($action);
    }

    //信号处理函数
    public function signalHandler($signo)
    {
        echo "" . getmypid() . " signalHandler($signo)\n";
        switch ($signo) {
            //子进程结束信号，可能会有多个子进程结束，用同一个信号，所以while里也要wait
            case SIGCHLD:
                $stopped_pid = pcntl_wait($status);
                echo "wait pid=".$stopped_pid,"\n";
                $this->_removePid($stopped_pid);
                break;
            //中断进程
            case SIGTERM:
            case SIGHUP:
            case SIGQUIT:
                $this->running = false;
                $this->_killAllChildren();
                break;
            default:
                break;
        }

    }

    /**
     * daemon start
     * @throws \Exception
     */
    public function actionStart()
    {
        $master_pid = @file_get_contents($this->pidFile);
        $master_is_alive = $master_pid && @posix_kill($master_pid, SIG_DFL);
        if ($master_is_alive) {
            echo $this->daemonConfig['masterName'].": already running\n";
            exit;
        }

        set_time_limit(0);

        umask(0); //把文件掩码清0

        $master_pid = pcntl_fork();

        if ($master_pid < 0) {
            echo "fork master failed!\n";
        } else if ($master_pid) {
            exit(0);
        }

        // 输出重定向到日志
        fclose(STDOUT);
        fclose(STDERR);
        global $STDOUT, $STDERR;
        $STDOUT = $this->infoLog ? fopen($this->infoLog, 'ab') :
            fopen(Yii::$app->runtimePath."/logs/console-".date('Y-m-d')."-info.log", 'ab');
        $STDERR = $this->errLog ? fopen($this->errLog, 'ab') :
            fopen(Yii::$app->runtimePath."/logs/console-".date('Y-m-d')."-err.log", 'ab');

        posix_setsid();//设置新会话组长，脱离终端

        cli_set_process_title($this->daemonConfig['masterName'].": master process");
        $this->saveMasterPid();

        $this->_onStart();
    }

    public function actionStop()
    {
        $master_pid = @file_get_contents($this->pidFile);
        posix_kill($master_pid, SIGTERM);
    }

    public function actionRestart()
    {
        self::actionStop();
        sleep(1);
        $master_pid = @file_get_contents($this->pidFile);
        while($master_is_alive = $master_pid && @posix_kill($master_pid, SIG_DFL))
        {
            sleep(1);
        }
        self::actionStart();
    }

    protected function saveMasterPid()
    {
        self::$_master_pid = posix_getpid();
        if (false === @file_put_contents($this->pidFile, self::$_master_pid)) {
            throw new \Exception('can not save pid to ' . $this->pidFile);
        }
    }

    /**
     * 检查环境是否支持pcntl
     *
     */
    private function _checkPcntl()
    {
        // Make sure PHP has support for pcntl
        if (!function_exists('pcntl_signal')) {
            $message = 'PHP does not appear to be compiled with the PCNTL extension.  This is neccesary for daemonization';
            echo $message;
            throw new \Exception($message);
        }
    }

    /**
     * kill all children
     */
    private function _killAllChildren()
    {
        foreach ($this->running_children_pid as $pname => $pids) {
            foreach ($pids as $pid) {
                posix_kill($pid, SIGQUIT);
            }
        }
    }

    //删除pid
    private function _removePid($stopped_pid)
    {
        foreach ($this->running_children_pid as $pname => $pids) {
            $index = array_search($stopped_pid, $pids);
            if ($index !== false && $index >= 0) {
                unset($this->running_children_pid[$pname][$index]);
                if (count($this->running_children_pid[$pname]) == 0) {
                    unset($this->running_children_pid[$pname]);
                }
            }
        }
    }

    private function _onStart()
    {
        while ($this->running) {
            //
            $process_name = $this->daemonConfig['masterName'];
            foreach ($this->daemonConfig['workers'] as $worker) {
                $controller = $worker["controller"];
                $count = intval($worker["count"]);
                $action = $controller . "/run";
                $pids = [];
                $running_count = 0;
                if (isset($this->running_children_pid[$worker['name']])) {//processes are running
                    $pids = $this->running_children_pid[$worker['name']];
                    $running_count = count($pids);
                }
                if ($running_count >= $count) {//reduce process, send signal
                    for ($i = 0; $i < $running_count - $count; ++$i) {
                        $stopping_pid = array_shift($pids);
                        posix_kill($stopping_pid, SIGQUIT);
                    }

                    $this->running_children_pid[$worker['name']] = $pids;
                }

                $params = [$worker['key']];

                for ($i = 0; $i < $count - $running_count; ++$i) {
                    $child_pid = pcntl_fork();
                    if ($child_pid < 0) {
                        continue;
                    } else if (0 == $child_pid) {
                        //worker
                        cli_set_process_title($process_name.": worker process: " . $worker['name']);
                        Yii::$app->runAction($action, $params);
                        exit(0);
                    }
                    //mgr
                    $this->running_children_pid[$worker['name']][] = $child_pid;
                }
            }

            //信号处理
            pcntl_signal(SIGTERM, array($this, "signalHandler"), false);
            pcntl_signal(SIGINT, array($this, "signalHandler"), false);
            pcntl_signal(SIGQUIT, array($this, "signalHandler"), false);
            pcntl_signal(SIGCHLD, array($this, "signalHandler"), false);
            self::saveMasterPid();
            pcntl_signal_dispatch();

            sleep(1);
        }

        while (count($this->running_children_pid)) {
            pcntl_signal_dispatch();
            $stopped_pid = pcntl_wait($status);
            echo "loop wait pid=".$stopped_pid,"\n";
            $this->_removePid($stopped_pid);

            sleep(1);
        }
    }
}